#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xutil.h>
#include <X11/extensions/Xcomposite.h>

#include <unordered_set>
#include <pthread.h>

#include <obs-module.h>
#include <util/platform.h>

#include "xcompcap-helper.hpp"

namespace XCompcap
{
	static Display* xdisplay = 0;

	Display *disp()
	{
		if (!xdisplay)
			xdisplay = XOpenDisplay(NULL);

		return xdisplay;
	}

	void cleanupDisplay()
	{
		if (!xdisplay)
			return;

		XCloseDisplay(xdisplay);
		xdisplay = 0;
	}

	static void getAllWindows(Window parent, std::list<Window>& windows)
	{
		UNUSED_PARAMETER(parent);
		UNUSED_PARAMETER(windows);
	}

	std::list<Window> getAllWindows()
	{
		std::list<Window> res;

		for (int i = 0; i < ScreenCount(disp()); ++i)
			getAllWindows(RootWindow(disp(), i), res);

		return res;
	}

	// Specification for checking for ewmh support at
	// http://standards.freedesktop.org/wm-spec/wm-spec-latest.html#idm140200472693600

	bool ewmhIsSupported()
	{
		Display *display = disp();
		Atom netSupportingWmCheck = XInternAtom(display,
				"_NET_SUPPORTING_WM_CHECK", true);
		Atom actualType;
		int format = 0;
		unsigned long num = 0, bytes = 0;
		unsigned char *data = NULL;
		Window ewmh_window = 0;

		int status = XGetWindowProperty(
				display,
				DefaultRootWindow(display),
				netSupportingWmCheck,
				0L,
				1L,
				false,
				XA_WINDOW,
				&actualType,
				&format,
				&num,
				&bytes,
				&data);

		if (status == Success) {
			if (num > 0) {
				ewmh_window = ((Window*)data)[0];
			}
			if (data) {
				XFree(data);
				data = NULL;
			}
		}

		if (ewmh_window) {
			status = XGetWindowProperty(
					display,
					ewmh_window,
					netSupportingWmCheck,
					0L,
					1L,
					false,
					XA_WINDOW,
					&actualType,
					&format,
					&num,
					&bytes,
					&data);
			if (status != Success || num == 0 ||
					ewmh_window != ((Window*)data)[0]) {
				ewmh_window = 0;
			}
			if (status == Success && data) {
				XFree(data);
			}
		}

		return ewmh_window != 0;
	}

	std::list<Window> getTopLevelWindows()
	{
		std::list<Window> res;

		if (!ewmhIsSupported()) {
			blog(LOG_WARNING, "Unable to query window list "
					"because window manager "
					"does not support extended "
					"window manager Hints");
			return res;
		}

		Atom netClList = XInternAtom(disp(), "_NET_CLIENT_LIST", true);
		Atom actualType;
		int format;
		unsigned long num, bytes;
		Window* data = 0;

		for (int i = 0; i < ScreenCount(disp()); ++i) {
			Window rootWin = RootWindow(disp(), i);

			int status = XGetWindowProperty(
							 disp(),
							 rootWin,
							 netClList,
							 0L,
							 ~0L,
							 false,
							 AnyPropertyType,
							 &actualType,
							 &format,
							 &num,
							 &bytes,
							 (uint8_t**)&data);

			if (status != Success) {
				blog(LOG_WARNING, "Failed getting root "
				                  "window properties");
				continue;
			}

			for (unsigned long i = 0; i < num; ++i)
				res.push_back(data[i]);

			XFree(data);
		}

		return res;
	}

	int getRootWindowScreen(Window root)
	{
		XWindowAttributes attr;

		if (!XGetWindowAttributes(disp(), root, &attr))
			return DefaultScreen(disp());

		return XScreenNumberOfScreen(attr.screen);
	}

	std::string getWindowName(Window win)
	{
		Atom netWmName = XInternAtom(disp(), "_NET_WM_NAME", false);
		int n;
		char **list = 0;
		XTextProperty tp;
		std::string res = "unknown";

		XGetTextProperty(disp(), win, &tp, netWmName);

		if (!tp.nitems)
			XGetWMName(disp(), win, &tp);

		if (!tp.nitems)
			return "error";

		if (tp.encoding == XA_STRING) {
			res = (char*)tp.value;
		} else {
			int ret = XmbTextPropertyToTextList(disp(), &tp, &list,
					&n);

			if (ret >= Success && n > 0 && *list) {
				res = *list;
				XFreeStringList(list);
			}
		}

		char *conv = nullptr;
		if (os_mbs_to_utf8_ptr(res.c_str(), 0, &conv)) 
			res = conv;
		bfree(conv);

		XFree(tp.value);

		return res;
	}

	std::string getWindowCommand(Window win)
	{
		Atom xi = XInternAtom(disp(), "WM_COMMAND", false);
		int n;
		char **list = 0;
		XTextProperty tp;
		std::string res = "error";

		XGetTextProperty(disp(), win, &tp, xi);

		if (!tp.nitems)
			return std::string();

		if (tp.encoding == XA_STRING) {
			res = (char*)tp.value;
		} else {
			int ret = XmbTextPropertyToTextList(disp(), &tp, &list,
					&n);
			if (ret >= Success && n > 0 && *list) {
				res = *list;
				XFreeStringList(list);
			}
		}

		XFree(tp.value);

		return res;
	}

	int getWindowPid(Window win)
	{
		UNUSED_PARAMETER(win);
		return 1234; //TODO
	}

	static std::unordered_set<Window> changedWindows;
	static pthread_mutex_t changeLock = PTHREAD_MUTEX_INITIALIZER;
	void processEvents()
	{
		PLock lock(&changeLock);

		XLockDisplay(disp());

		while (XEventsQueued(disp(), QueuedAfterReading) > 0) {
			XEvent ev;

			XNextEvent(disp(), &ev);

			if (ev.type == ConfigureNotify)
				changedWindows.insert(ev.xconfigure.event);

			if (ev.type == MapNotify)
				changedWindows.insert(ev.xmap.event);

			if (ev.type == Expose)
				changedWindows.insert(ev.xexpose.window);

			if (ev.type == DestroyNotify)
				changedWindows.insert(ev.xdestroywindow.event);
		}

		XUnlockDisplay(disp());
	}

	bool windowWasReconfigured(Window win)
	{
		PLock lock(&changeLock);

		auto it = changedWindows.find(win);

		if (it != changedWindows.end()) {
			changedWindows.erase(it);
			return true;
		}

		return false;
	}

}


PLock::PLock(pthread_mutex_t* mtx, bool trylock)
	:m(mtx)
{
	if (trylock)
		islock = mtx && pthread_mutex_trylock(mtx) == 0;
	else
		islock = mtx && pthread_mutex_lock(mtx) == 0;
}

PLock::~PLock()
{
	if (islock) {
		pthread_mutex_unlock(m);
	}
}

bool PLock::isLocked()
{
	return islock;
}

void PLock::unlock()
{
	if (islock) {
		pthread_mutex_unlock(m);
		islock = false;
	}
}

void PLock::lock()
{
	if (!islock) {
		pthread_mutex_lock(m);
		islock = true;
	}
}



static bool* curErrorTarget = 0;
static char curErrorText[200];
static int xerrorlock_handler(Display* disp, XErrorEvent* err)
{

	if (curErrorTarget)
		*curErrorTarget = true;

	XGetErrorText(disp, err->error_code, curErrorText, 200);

	return 0;
}

XErrorLock::XErrorLock()
{
	goterr = false;
	islock = false;
	prevhandler = 0;

	lock();
}

XErrorLock::~XErrorLock()
{
	unlock();
}

bool XErrorLock::isLocked()
{
	return islock;
}

void XErrorLock::lock()
{
	if (!islock) {
		XLockDisplay(XCompcap::disp());
		XSync(XCompcap::disp(), 0);

		curErrorTarget = &goterr;
		curErrorText[0] = 0;
		prevhandler = XSetErrorHandler(xerrorlock_handler);

		islock = true;
	}
}

void XErrorLock::unlock()
{
	if (islock) {
		XSync(XCompcap::disp(), 0);

		curErrorTarget = 0;
		XSetErrorHandler(prevhandler);
		prevhandler = 0;
		XUnlockDisplay(XCompcap::disp());
		islock = false;
	}
}

bool XErrorLock::gotError()
{
	if (!islock)
		return false;

	XSync(XCompcap::disp(), 0);

	bool res = goterr;
	goterr = false;
	return res;
}

std::string XErrorLock::getErrorText()
{
	return curErrorText;
}

void XErrorLock::resetError()
{
	if (islock)
		XSync(XCompcap::disp(), 0);

	goterr = false;
	curErrorText[0] = 0;
}

XDisplayLock::XDisplayLock()
{
	islock = false;
	lock();
}

XDisplayLock::~XDisplayLock()
{
	unlock();
}

bool XDisplayLock::isLocked()
{
	return islock;
}

void XDisplayLock::lock()
{
	if (!islock) {
		XLockDisplay(XCompcap::disp());
		islock = true;
	}
}

void XDisplayLock::unlock()
{
	if (islock) {
		XSync(XCompcap::disp(), 0);
		XUnlockDisplay(XCompcap::disp());
		islock = false;
	}
}


ObsGsContextHolder::ObsGsContextHolder()
{
	obs_enter_graphics();
}

ObsGsContextHolder::~ObsGsContextHolder()
{
	obs_leave_graphics();
}
